package top.ivansong.crawler.weather.support;

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import top.ivansong.crawler.weather.bo.WeatherCNData;
import top.ivansong.crawler.weather.bo.WeatherHourlyCNData;
import top.ivansong.crawler.weather.bo.WeatherHourlyMJData;
import top.ivansong.crawler.weather.bo.WeatherMJData;

import java.io.IOException;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author ivansong
 * @since 2018/2/23
 */
@Component
@Slf4j
public class FetchWeatherSupport {
    private static String chengduCNUrl = "http://www.weather.com.cn/weather/101270101.shtml";

    private static String chengduMJUrl = "https://tianqi.moji.com/weather/china/sichuan/chengdu";

    private static String chengduMJCookie = "moji_setting=%7B%22internal_id%22%3A53%7D";

    private static String MJ24HUrl = "https://tianqi.moji.com/index/getHour24";

    @Autowired
    private RestTemplate restTemplate;

    private static final Gson gson = new Gson();

    public List<WeatherHourlyCNData> fetchWeatherHourlyDataOfCN() {
        LinkedList<WeatherHourlyCNData> dayDatas = new LinkedList<>();
        try {
            Document doc = generateDoc(chengduCNUrl);
            Map<Integer, String> weatherContent = new HashMap<>();

            for (Element element : doc.getElementsByTag("script")) {
                if (element.childNodeSize() == 0) {
                    continue;
                }

                String childNode = element.childNode(0).toString();
                if (StringUtils.isBlank(childNode)) {
                    continue;
                }
                if (childNode.contains("observe24h_data")) {
                    FetchWeatherHourlyCNJsonData jsonData = Optional.of(childNode.split("="))
                            .filter(s -> s.length > 0)
                            .map(s -> s[1])
                            .map(s -> s.replace(";", ""))
                            .map(s -> gson.fromJson(s, FetchWeatherHourlyCNJsonData.class))
                            .orElse(null);
                    if (jsonData == null) {
                        continue;
                    }

                    jsonData.getOd().getOd2().forEach(e -> {
                        if (StringUtils.isNotEmpty(e.getOd28()) && StringUtils.isNotEmpty(e.getOd21()) && StringUtils.isNotEmpty(e.getOd22())) {
                            WeatherHourlyCNData dayData = new WeatherHourlyCNData();
                            dayData.setTime(Integer.parseInt(e.getOd21()));
                            dayData.setTemperature(Integer.parseInt(e.getOd22()));
                            dayData.setAqi(Integer.parseInt(e.getOd28()));
                            dayData.setHumidity(Integer.parseInt(e.getOd27()));
                            dayData.setWind(e.getOd24());
                            dayData.setWindLevel(Integer.parseInt(e.getOd25()));
                            dayDatas.addFirst(dayData);
                        }
                    });
                } else if (childNode.contains("hour3data")) {
                    FetchWeather3HourlyCNJsonData jsonData = Optional.of(childNode.split("="))
                                    .filter(s -> s.length > 0)
                                    .map(s -> s[1])
                                    .map(s -> s.replace(";", ""))
                                    .map(s -> gson.fromJson(s, FetchWeather3HourlyCNJsonData.class))
                                    .orElse(null);
                    if (jsonData == null || CollectionUtils.isEmpty(jsonData.getOndDay())) {
                        continue;
                    }

                    int time = 8;

                    for (String str : jsonData.getOndDay()) {
                        String weather = Optional.of(str.split(","))
                                .filter(ss -> ss.length > 3)
                                .map(ss -> ss[2])
                                .orElse("");
                        weatherContent.put(time, weather);
                        weatherContent.put(time + 1, weather);
                        weatherContent.put(time + 2, weather);
                        time += 3;
                    }
                }
            }

            for (int i = 1; i < dayDatas.size(); i++) {
                Integer curTime = dayDatas.get(i - 1).getTime() + 1;
                WeatherHourlyCNData data = dayDatas.get(i);
                data.setTime(curTime);
                data.setContent(weatherContent.get(curTime));
            }
        } catch (Exception e) {
            log.error("fetch cn hourly weather error ", e);
        }
        return dayDatas;

    }

    private Document generateDoc(String url) throws IOException {
        Connection connection = Jsoup.connect(url);
        return connection.get();
    }


    public List<WeatherCNData> fetchWeatherDataOfCN() {
        List<WeatherCNData> res = new ArrayList<>();
        try {
            LocalDate curr = LocalDate.now();

            Elements sevenDayElements = Optional.ofNullable(generateDoc(chengduCNUrl))
                    .map(doc -> doc.getElementById("7d"))
                    .map(ele -> ele.getElementsByClass("clearfix"))
                    .map(Elements::first)
                    .map(ele -> ele.getElementsByTag("li"))
                    .orElse(null);
            if (sevenDayElements == null || sevenDayElements.isEmpty()) {
                return res;
            }

            for (Element element : sevenDayElements) {
                if (element.childNodeSize() == 0) {
                    continue;
                }

                WeatherCNData cnData = new WeatherCNData();
                cnData.setDate(curr);
                curr = curr.plusDays(1L);

                handleWeatherCNElement(element, cnData);
                res.add(cnData);
            }
        } catch (Exception e) {
            log.error("fetch cn hourly weather error ", e);
        }
        return res;
    }

    private void handleWeatherCNElement(Element element, WeatherCNData cnData) {
        String content = Optional.ofNullable(element.getElementsByClass("wea"))
                .map(Elements::first)
                .map(e -> e.attr("title"))
                .orElse("");
        cnData.setContent(content);

        Element tempElement = Optional.ofNullable(element.getElementsByClass("tem"))
                .map(Elements::first)
                .orElse(null);

        Integer maxTemp = Optional.ofNullable(tempElement)
                .map(e -> e.getElementsByTag("span"))
                .map(Elements::first)
                .map(Element::text)
                .filter(StringUtils::isNotBlank)
                .map(Integer::valueOf)
                .orElse(null);
        cnData.setMaxTemperature(maxTemp);

        Integer minTemp = Optional.ofNullable(tempElement)
                .map(e -> e.getElementsByTag("i"))
                .map(Elements::first)
                .map(Element::text)
                .map(FetchWeatherSupport::parseIntegerValue)
                .filter(StringUtils::isNotBlank)
                .map(Integer::valueOf)
                .orElse(null);
        cnData.setMinTemperature(minTemp);

        Element windElement = Optional.ofNullable(element.getElementsByClass("win"))
                .map(Elements::first)
                .orElse(null);
        String wind = Optional.ofNullable(windElement)
                .map(e -> e.getElementsByTag("span"))
                .filter(e -> !e.isEmpty())
                .map(elements -> {
                    Set<String> windDicCollect = new HashSet<>();
                    for (Element ele : elements) {
                        String attr = ele.attr("title");
                        if (StringUtils.isNotBlank(attr)) {
                            windDicCollect.add(attr);
                        }
                    }
                    return windDicCollect;
                })
                .map(e -> String.join(",", e))
                .orElse("");
        cnData.setWind(wind);

        String windLevel = Optional.ofNullable(windElement)
                .map(e -> e.getElementsByTag("i"))
                .map(Elements::first)
                .map(Element::text)
                .orElse("");
        cnData.setWindLevel(windLevel);
    }


    public List<WeatherHourlyMJData> fetchWeatherHourlyDataOfMJ() {
        List<WeatherHourlyMJData> dayDatas = new ArrayList<>();

        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Cookie", chengduMJCookie);
            HttpEntity<Void> httpEntity = new HttpEntity<>(headers);

            ResponseEntity<String> responseEntity = restTemplate.exchange(MJ24HUrl, HttpMethod.GET, httpEntity, String.class);
            if (responseEntity.getStatusCode() == HttpStatus.OK) {
                dayDatas = Optional.ofNullable(responseEntity.getBody())
                        .map(s -> new Gson().fromJson(responseEntity.getBody(), FetchWeatherHourlyMJResponse.class))
                        .map(FetchWeatherHourlyMJResponse::getHour24)
                        .map(list -> list.stream()
                                .map(e -> WeatherHourlyMJData.builder()
                                        .content(e.getCondition())
                                        .humidity(e.getHumidity())
                                        .temperature(e.getTemperature())
                                        .time(e.getHour())
                                        .wind(e.getWindDir())
                                        .windLevel(e.getWindLevel())
                                        .build())
                                .collect(Collectors.toList()))
                        .orElse(Collections.emptyList());

            }
        } catch (Exception e) {
            log.error("fetch cn hourly weather error ", e);
        }
        return dayDatas;

    }


    public List<WeatherMJData> fetchWeatherDataOfMJ() {
        List<WeatherMJData> res = new ArrayList<>();
        try {
            LocalDate curr = LocalDate.now();
            Elements daysElements = Optional.ofNullable(generateDoc(chengduMJUrl))
                    .map(doc -> doc.getElementsByClass("forecast"))
                    .map(Elements::first)
                    .map(ele -> ele.getElementsByClass("days"))
                    .orElse(null);
            if (daysElements == null || daysElements.isEmpty()) {
                return res;
            }

            for (Element element : daysElements) {
                if (element.childNodeSize() == 0) {
                    continue;
                }

                Elements dayData = Optional.ofNullable(element.getElementsByTag("li"))
                        .orElse(null);
                if (dayData == null || dayData.size() < 5) {
                    continue;
                }

                WeatherMJData cnData = new WeatherMJData();
                cnData.setDate(curr);
                curr = curr.plusDays(1L);

                handleMJDayDataElement(dayData, cnData);
                res.add(cnData);
            }
        } catch (Exception e) {
            log.error("fetch cn hourly weather error ", e);
        }
        return res;
    }

    private void handleMJDayDataElement(Elements dayData, WeatherMJData cnData) {
        String content = dayData.get(1).text();
        cnData.setContent(content);

        String[] temArr = Optional.ofNullable(dayData.get(2).text())
                .map(s -> s.split("/"))
                .orElse(null);
        Integer maxTemp = Optional.ofNullable(temArr)
                .filter(e -> e.length > 1)
                .map(e -> e[1])
                .map(FetchWeatherSupport::parseIntegerValue)
                .map(Integer::valueOf)
                .orElse(null);
        cnData.setMaxTemperature(maxTemp);
        Integer minTem = Optional.ofNullable(temArr)
                .filter(e -> e.length > 0)
                .map(e -> e[0])
                .map(FetchWeatherSupport::parseIntegerValue)
                .map(Integer::valueOf)
                .orElse(null);
        cnData.setMinTemperature(minTem);

        Element windElement = dayData.get(3);
        String wind = Optional.ofNullable(windElement.getElementsByTag("em"))
                .map(Elements::first)
                .map(Element::text)
                .orElse("");
        cnData.setWind(wind);
        String windLevel = Optional.ofNullable(windElement.getElementsByTag("b"))
                .map(Elements::first)
                .map(Element::text)
                .orElse("");
        cnData.setWindLevel(windLevel);

        Integer aqi = Optional.ofNullable(dayData.get(4))
                .map(ele -> ele.getElementsByTag("strong"))
                .map(Elements::first)
                .map(Element::text)
                .map(FetchWeatherSupport::parseIntegerValue)
                .filter(StringUtils::isNotBlank)
                .map(Integer::valueOf)
                .orElse(null);
        cnData.setAqi(aqi);
    }


    private static String parseIntegerValue(String s) {
        if (StringUtils.isBlank(s)) {
            return "";
        }

        StringBuilder res = new StringBuilder();
        for (char ch : s.toCharArray()) {
            if (ch >= 48 && ch <= 57) {
                res.append(ch);
            }
        }
        return res.toString();
    }


    private static class FetchWeatherHourlyCNJsonData implements Serializable {
        private static final long serialVersionUID = 5211967491472838015L;

        private FetchWeatherHourlyCNODJsonData od;

        public FetchWeatherHourlyCNODJsonData getOd() {
            return od;
        }

        public void setOd(FetchWeatherHourlyCNODJsonData od) {
            this.od = od;
        }
    }

    @Getter
    @Setter
    private static class FetchWeather3HourlyCNJsonData implements Serializable {
        private static final long serialVersionUID = 5211967491472838015L;
        @SerializedName(value = "1d")
        private List<String> ondDay;
        @SerializedName(value = "7d")
        private List<List<String>> sevenDay;
    }


    private static class FetchWeatherHourlyCNODJsonData implements Serializable {
        private static final long serialVersionUID = 5211967491472838015L;

        private List<FetchWeatherHourlyCNOD2JsonData> od2;

        public List<FetchWeatherHourlyCNOD2JsonData> getOd2() {
            return od2;
        }

        public void setOd2(List<FetchWeatherHourlyCNOD2JsonData> od2) {
            this.od2 = od2;
        }
    }

    @Getter
    @Setter
    private static class FetchWeatherHourlyCNOD2JsonData implements Serializable {
        private static final long serialVersionUID = 5211967491472838015L;
        private String od21;
        private String od22;
        private String od28;
        private String od27;
        private String od24;
        private String od25;
    }

    private static class DayData implements Serializable {
        private static final long serialVersionUID = 5211967491472838015L;
        private int time;
        private int temperature;
        private int AQI;

        public int getTime() {
            return time;
        }

        public void setTime(int time) {
            this.time = time;
        }

        public int getTemperature() {
            return temperature;
        }

        public void setTemperature(int temperature) {
            this.temperature = temperature;
        }

        public int getAQI() {
            return AQI;
        }

        public void setAQI(int AQI) {
            this.AQI = AQI;
        }
    }


    @Getter
    @Setter
    private static class FetchWeatherHourlyMJResponse implements Serializable {
        private static final long serialVersionUID = 0L;
        private List<FetchWeatherHourlyMJJsonData> hour24;
    }

    @Getter
    @Setter
    private static class FetchWeatherHourlyMJJsonData implements Serializable {
        private static final long serialVersionUID = 0L;
        @SerializedName("Fcondition")
        private String condition;
        @SerializedName("Fhumidity")
        private Integer humidity;
        @SerializedName("Fpredict_hour")
        private Integer hour;
        @SerializedName("Ftemp")
        private Integer temperature;
        @SerializedName("Fwdir")
        private String windDir;
        @SerializedName("wind_level")
        private Integer windLevel;
    }

}
